﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;

namespace kdata
{

	[KSPAddon(KSPAddon.Startup.Flight, false)]
	public class FlightDataPlugin : MonoBehaviour
	{

		// These objects hold the stats we're preparing for display
		private DisplayItem period = new DisplayItem("Period:");
		private DisplayItem inclination = new DisplayItem("Inclination:");
		private DisplayItem apoapsis = new DisplayItem("Apoapsis:");
		private DisplayItem periapsis = new DisplayItem("Periapsis:");
		private DisplayItem altitude = new DisplayItem("Altitude (sea level):");
		private DisplayItem altitudeR = new DisplayItem("Altitude (radar):");
		private DisplayItem biome = new DisplayItem("Biome:");

		// The screen boundaries we print everything to
		private static float left = (Screen.width / 2) - 600;
		private static float top = Screen.height - 135;
		private static float width = 360;
		private static float height = 105;

		// the level of transparency from 0 (see through) to 1 (opaque)
		private static float transparency = 0.6f;

		// the screen object we print to
		private static Rect layoutArea = new Rect(left, top, width, height);

		// the background texture
		private static bool bgBuilt = false;
		private static Texture2D bgTex = new Texture2D((int)width, (int)height);

		// whether or not to show the GUI (toggles with F2 key pressing)
		private bool showGUI = true;


		// ===============================================================================
		// BEGIN KSP HOOKS
		//
		//		called by KSP when certain events take place, we emplement 
		//		these functions to make sure we can respond to the events
		// ===============================================================================


		// public hook called by KSP once at game/scene starts
		public void Start()
		{
			buildBackground();
			RenderingManager.AddToPostDrawQueue(3, OnGUI);
		}

		// public hook called by KSP when things change we need to know about (i think)
		public void Update()
		{

			if (Input.GetKeyDown(KeyCode.F2))
			{
				showGUI = !showGUI;
			}

		}


		// public hook called by KSP at regular intervals to do physics calculations etc
		// we take this oportunity to recalculate our items for display
		public void FixedUpdate()
		{
			// only bother if a ship is actually active
			if (FlightGlobals.ActiveVessel != null)
			{
				Orbit o = FlightGlobals.ActiveVessel.orbit;
				prepareAltitude(o);
				prepareApoapsis(o);
				preparePeriapsis(o);
				checkApPeColours(o);
				prepareInclination(o);
				preparePeriod(o);
				prepareBiome();
			}
		}

		// Public hook called by KSP every time the GUI is re-rendered
		public void OnGUI()
		{
			if (FlightGlobals.ActiveVessel != null && showGUI)
			{
				layoutArea = GUILayout.Window(1004, layoutArea, paintWindow,"KData - Flight Instruments");
			}
		}

		void paintWindow(int windowID)
		{
			GUILayout.BeginVertical();
			printDisplayItem(altitudeR);
			printDisplayItem(altitude);
			printDisplayItem(apoapsis);
			printDisplayItem(periapsis);
			printDisplayItem(inclination);
			printDisplayItem(period);
			printDisplayItem(biome);
			GUILayout.EndVertical();
			GUI.DragWindow();
		}

		void paintArea()
		{
			GUILayout.BeginArea(layoutArea, bgTex);
			GUILayout.BeginVertical();
			printDisplayItem(altitudeR);
			printDisplayItem(altitude);
			printDisplayItem(apoapsis);
			printDisplayItem(periapsis);
			printDisplayItem(inclination);
			printDisplayItem(period);
			printDisplayItem(biome);
			GUILayout.EndVertical();
			GUILayout.EndArea();
		}


		// ===============================================================================
		// Private functions to do the things our plugin does, i.e. not called by KSP
		// ===============================================================================

		// fetches and formats the biome name, this code based heavily on an extract 
		// from something Toadicus wrote (thanks Toadicus)
		private void prepareBiome()
		{

			CBAttributeMapSO.MapAttribute mapAttribute;

			try
			{
				Vessel vessel = FlightGlobals.ActiveVessel;
				CBAttributeMapSO BiomeMap = vessel.mainBody.BiomeMap;
				double lat = vessel.latitude * Math.PI / 180d;
				double lon = vessel.longitude * Math.PI / 180d;
				mapAttribute = BiomeMap.GetAtt(lat, lon);
			}
			catch (NullReferenceException)
			{
				mapAttribute = new CBAttributeMapSO.MapAttribute();
				mapAttribute.name = "N/A";
			}

			this.biome.value = mapAttribute.name;

		}

		// formats the inclination data for display
		private void prepareInclination(Orbit o)
		{
			this.inclination.value = string.Format("{0:000.00}°", o.inclination);
		}

		// formats the period data for display *snicker*
		private void preparePeriod(Orbit o)
		{

			this.period.value = formatTimespan(TimeSpan.FromSeconds(o.period));
			this.period.valueStyle.normal.textColor = Color.green;

			int bodyPeriod = (int)Math.Floor(FlightGlobals.ActiveVessel.mainBody.rotationPeriod);
			int halfPeriod = (int)Math.Floor(FlightGlobals.ActiveVessel.mainBody.rotationPeriod / 2);
			int thirdPeriod = (int)Math.Floor(FlightGlobals.ActiveVessel.mainBody.rotationPeriod / 3);
			int quarterPeriod = (int)Math.Floor(FlightGlobals.ActiveVessel.mainBody.rotationPeriod / 4);
			int vesselPeriod =(int) Math.Floor(o.period);
			Color c = Color.green;

			if (vesselPeriod == bodyPeriod)
			{
				this.period.value += " GEO";
				c = Color.cyan;
			}
			else if (vesselPeriod == halfPeriod)
			{
				this.period.value += " GEO/2";
				c = Color.cyan;
			}
			else if (vesselPeriod == quarterPeriod)
			{
				this.period.value += " GEO/4";
				c = Color.cyan;
			}
			else if (vesselPeriod == thirdPeriod)
			{
				this.period.value += " GEO/3";
				c = Color.cyan;
			}

			this.period.valueStyle.normal.textColor = c;

		}

		// alters the colour of the AP and PE if they are close to circular
		private void checkApPeColours(Orbit o) {
			if(Math.Abs(o.PeA - o.ApA) < (o.ApA / 1000)) {
				this.apoapsis.valueStyle.normal.textColor = Color.cyan;
				this.periapsis.valueStyle.normal.textColor = Color.cyan;
			}
		}

		// formats the Ap data for display
		private void prepareApoapsis(Orbit o)
		{
			string timeToAp = formatTimespan(TimeSpan.FromSeconds(o.timeToAp));
			this.apoapsis.value = Math.Floor(o.ApA).ToString("#,##0") + "m (" + timeToAp + ")";

			if (Math.Floor(o.ApA) <= 0)
			{
				this.apoapsis.valueStyle.normal.textColor = Color.red;
			} 
			else if (Math.Floor(o.ApA) <= FlightGlobals.ActiveVessel.mainBody.maxAtmosphereAltitude)
			{
				this.apoapsis.valueStyle.normal.textColor = Color.yellow;
			}
			else 
			{
				this.apoapsis.valueStyle.normal.textColor = Color.green;
			}


		}

		// formats the Pe data for display
		private void preparePeriapsis(Orbit o)
		{
			string timeToPe = formatTimespan(TimeSpan.FromSeconds(o.timeToPe));
			this.periapsis.value = Math.Floor(o.PeA).ToString("#,##0") + "m (" + timeToPe + ")";

			if (Math.Floor(o.PeA) <= 0)
			{
				this.periapsis.valueStyle.normal.textColor = Color.red;
			}
			else if (Math.Floor(o.PeA) <= FlightGlobals.ActiveVessel.mainBody.maxAtmosphereAltitude)
			{
				this.periapsis.valueStyle.normal.textColor = Color.yellow;
			}
			else
			{
				this.periapsis.valueStyle.normal.textColor = Color.green;
			}

		}

		// formats the Altitude data for display, this does both "radar" and "sea level" altitudes
		private void prepareAltitude(Orbit o)
		{

			double terrain = FlightGlobals.ActiveVessel.terrainAltitude;
			long radarAltitude;

			if (terrain < 0) terrain = 0;

			this.altitude.value = FlightGlobals.ActiveVessel.altitude.ToString("#,##0") + "m";
			if (FlightGlobals.ActiveVessel.altitude <= FlightGlobals.ActiveVessel.mainBody.maxAtmosphereAltitude)
			{
				this.altitude.valueStyle.normal.textColor = Color.yellow;
			}
			else
			{
				this.altitude.valueStyle.normal.textColor = Color.green;
			}

			if (o.altitude < 30000)
			{

				radarAltitude = (long)(FlightGlobals.ActiveVessel.mainBody.GetAltitude(FlightGlobals.ActiveVessel.CoM) - terrain);
				this.altitudeR.value = radarAltitude.ToString("#,##0") + "m";

				if (radarAltitude < 1000)
				{
					this.altitudeR.valueStyle.normal.textColor = Color.red;
				}
				else if (radarAltitude <= FlightGlobals.ActiveVessel.mainBody.maxAtmosphereAltitude)
				{
					this.altitudeR.valueStyle.normal.textColor = Color.yellow;
				}
				else
				{
					this.altitudeR.valueStyle.normal.textColor = Color.green;
				}

			}
			else
			{
				this.altitudeR.value = "N/A";
				this.altitudeR.valueStyle.normal.textColor = Color.green;
			}


		}

		// a helper that re-paints a line in the dispaly area
		private void printDisplayItem(DisplayItem item)
		{
			int divider = 125; // the point on screen to stop column one and start column two
			GUILayoutOption colA = GUILayout.Width(divider);
			GUILayoutOption colB = GUILayout.Width(width - divider);
			GUILayout.BeginHorizontal();
			GUILayout.Label(item.label, item.labelStyle, colA);
			GUILayout.Label(item.value, item.valueStyle, colB);
			GUILayout.EndHorizontal();
		}

		// a helper to consistantly format time spans
		private string formatTimespan(TimeSpan t)
		{

			if (t.TotalDays >= 2)
			{
				// 15d 13h
				return string.Format("{0:D2}d {1:D2}h", t.Days, t.Hours);
			}

			if (t.TotalHours >= 1)
			{
				// 13h 20min 23.123s
				return string.Format("{0:D1}h {1:D1}min {2:D1}.{3:D1}s", t.Hours, t.Minutes, t.Seconds, t.Milliseconds / 100);
			}
			
			if (t.TotalMinutes >= 1)
			{
				// 13h 20min 23.123s
				return string.Format("{0:D1}min {1:D1}.{2:D1}s", t.Minutes, t.Seconds, t.Milliseconds / 100);
			}

			// 13h 20min 23.123s
			return string.Format("{0:D1}.{1:D1}s", t.Seconds, t.Milliseconds / 100);

		}

		// creates a texture (read background image) for the display area, 
		// this is the see through black stuff behind the text we're printing
		private void buildBackground()
		{

			// don't do this more than once, it's not needed
			if (bgBuilt)
			{
				return;
			}

			bgBuilt = true;
			Color c = Color.black;
			c.a = transparency;

			for (int x = 0; x <= width; x++)
			{
				for (int y = 0; y <= height; y++)
				{
					bgTex.SetPixel(x, y, c);
				}
			}

			bgTex.Apply();

		}


	}

}
